<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>nodejs中的异步、非阻塞I/O是如何实现的 | yanyan</title>
    <meta name="description" content="Yan&#39;s blog">
    <meta name="generator" content="VuePress 1.4.0">
    <script src="https://cdn.bootcss.com/jquery/3.5.0/jquery.slim.min.js"></script>
  <script src="https://cdn.bootcss.com/fancybox/3.5.7/jquery.fancybox.min.js"></script>
  <link rel="stylesheet" type="text/css" href="https://cdn.bootcss.com/fancybox/3.5.7/jquery.fancybox.min.css">
  <link rel="shortcut icon" type="image/x-icon" href="./favicon.ico">
    
    <link rel="preload" href="/assets/css/0.styles.e1b3f17d.css" as="style"><link rel="preload" href="/assets/js/app.e358a08d.js" as="script"><link rel="preload" href="/assets/js/2.88fa18d1.js" as="script"><link rel="preload" href="/assets/js/36.88b681f1.js" as="script"><link rel="prefetch" href="/assets/js/10.23baf844.js"><link rel="prefetch" href="/assets/js/11.45c148ba.js"><link rel="prefetch" href="/assets/js/12.e5930132.js"><link rel="prefetch" href="/assets/js/13.0547cd14.js"><link rel="prefetch" href="/assets/js/14.3e67795b.js"><link rel="prefetch" href="/assets/js/15.51129890.js"><link rel="prefetch" href="/assets/js/16.6987f89d.js"><link rel="prefetch" href="/assets/js/17.2807cff5.js"><link rel="prefetch" href="/assets/js/18.855e1707.js"><link rel="prefetch" href="/assets/js/19.6da24791.js"><link rel="prefetch" href="/assets/js/20.e24d4aef.js"><link rel="prefetch" href="/assets/js/21.6efc6fba.js"><link rel="prefetch" href="/assets/js/22.10447f0f.js"><link rel="prefetch" href="/assets/js/23.9154cc24.js"><link rel="prefetch" href="/assets/js/24.9ad529fc.js"><link rel="prefetch" href="/assets/js/25.4c092e0a.js"><link rel="prefetch" href="/assets/js/26.debdaa01.js"><link rel="prefetch" href="/assets/js/27.8b90b660.js"><link rel="prefetch" href="/assets/js/28.1a323e01.js"><link rel="prefetch" href="/assets/js/29.6f108fc9.js"><link rel="prefetch" href="/assets/js/3.7210d3aa.js"><link rel="prefetch" href="/assets/js/30.e7df1937.js"><link rel="prefetch" href="/assets/js/31.2cb3120f.js"><link rel="prefetch" href="/assets/js/32.eb64932c.js"><link rel="prefetch" href="/assets/js/33.cac3e2f0.js"><link rel="prefetch" href="/assets/js/34.19ea35c4.js"><link rel="prefetch" href="/assets/js/35.fadf5d03.js"><link rel="prefetch" href="/assets/js/37.2a799db9.js"><link rel="prefetch" href="/assets/js/38.2741a2bf.js"><link rel="prefetch" href="/assets/js/39.359ceb72.js"><link rel="prefetch" href="/assets/js/4.9e938666.js"><link rel="prefetch" href="/assets/js/40.56fd4a10.js"><link rel="prefetch" href="/assets/js/41.e72117ad.js"><link rel="prefetch" href="/assets/js/42.63a6e190.js"><link rel="prefetch" href="/assets/js/43.c8072421.js"><link rel="prefetch" href="/assets/js/44.84cd8367.js"><link rel="prefetch" href="/assets/js/45.0ac810b0.js"><link rel="prefetch" href="/assets/js/46.bb83ff34.js"><link rel="prefetch" href="/assets/js/47.a9333a81.js"><link rel="prefetch" href="/assets/js/48.526b5494.js"><link rel="prefetch" href="/assets/js/49.73b61cc6.js"><link rel="prefetch" href="/assets/js/5.88b252c7.js"><link rel="prefetch" href="/assets/js/50.f34ab799.js"><link rel="prefetch" href="/assets/js/51.d06a49d9.js"><link rel="prefetch" href="/assets/js/52.348d5482.js"><link rel="prefetch" href="/assets/js/6.0face56b.js"><link rel="prefetch" href="/assets/js/7.31eca58d.js"><link rel="prefetch" href="/assets/js/8.69e9ce95.js"><link rel="prefetch" href="/assets/js/9.f25df9e1.js">
    <link rel="stylesheet" href="/assets/css/0.styles.e1b3f17d.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div class="theme-container"><header class="navbar"><div class="sidebar-button"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 448 512" class="icon"><path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path></svg></div> <a href="/" class="home-link router-link-active"><!----> <span class="site-name">yanyan</span></a> <div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""> <!----></div> <!----></div></header> <div class="sidebar-mask"></div> <aside class="sidebar"><!---->  <ul class="sidebar-links"><li><section class="sidebar-group collapsable depth-0"><a href="/html/001" class="sidebar-heading clickable"><span>HTML</span> <span class="arrow right"></span></a> <!----></section></li><li><section class="sidebar-group collapsable depth-0"><a href="/css/image" class="sidebar-heading clickable"><span>CSS</span> <span class="arrow right"></span></a> <!----></section></li><li><section class="sidebar-group collapsable depth-0"><a href="/js/001" class="sidebar-heading clickable"><span>JS-基础</span> <span class="arrow right"></span></a> <!----></section></li><li><section class="sidebar-group collapsable depth-0"><a href="/js-v8/001" class="sidebar-heading clickable"><span>JS-V8引擎原理 </span> <span class="arrow right"></span></a> <!----></section></li><li><section class="sidebar-group collapsable depth-0"><a href="/js-async/001" class="sidebar-heading clickable open active"><span>JS-异步I/O及异步编程</span> <span class="arrow down"></span></a> <ul class="sidebar-links sidebar-group-items"><li><a href="/js-async/001.html" class="active sidebar-link">01.nodejs中的异步、非阻塞I/O是如何实现的？</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/js-async/001.html#什么是i-o？" class="sidebar-link">什么是I/O？</a></li><li class="sidebar-sub-header"><a href="/js-async/001.html#阻塞和非阻塞i-o" class="sidebar-link">阻塞和非阻塞I/O</a></li><li class="sidebar-sub-header"><a href="/js-async/001.html#异步-i-o-的本质" class="sidebar-link">异步 I/O 的本质</a></li></ul></li><li><a href="/js-async/002.html" class="sidebar-link">02.JS异步编程有哪些方案？为什么会出现这些方案？</a></li><li><a href="/js-async/003.html" class="sidebar-link">03.能不能简单实现一下 node 中回调函数的机制？</a></li><li><a href="/js-async/004.html" class="sidebar-link">04.Promise之问(一)——Promise 凭借什么消灭了回调地狱？</a></li><li><a href="/js-async/005.html" class="sidebar-link">05.Promise之问(一)——Promise之问(二)——为什么Promise要引入微任务？</a></li><li><a href="/js-async/006.html" class="sidebar-link">06.Promise之问(三)——Promise 如何实现链式调用？</a></li><li><a href="/js-async/007.html" class="sidebar-link">07.Promise之问(四)——实现Promise的 resolve、reject 和 finally</a></li><li><a href="/js-async/008.html" class="sidebar-link">08.Promise 之问(五)——实现Promise的 all 和 race</a></li><li><a href="/js-async/009.html" class="sidebar-link">09.谈谈你对生成器以及协程的理解。</a></li><li><a href="/js-async/010.html" class="sidebar-link">10.如何让 Generator 的异步代码按顺序执行完毕？</a></li><li><a href="/js-async/011.html" class="sidebar-link">11.解释一下async/await的运行机制。？</a></li><li><a href="/js-async/012.html" class="sidebar-link">12.forEach 中用 await 会产生什么问题?怎么解决这个问题？</a></li></ul></section></li><li><section class="sidebar-group collapsable depth-0"><a href="/http/000" class="sidebar-heading clickable"><span>HTTP</span> <span class="arrow right"></span></a> <!----></section></li><li><section class="sidebar-group collapsable depth-0"><a href="/extend/002" class="sidebar-heading clickable"><span>拓展阅读</span> <span class="arrow right"></span></a> <!----></section></li></ul> </aside> <main class="page"> <div class="theme-default-content content__default"><h1 id="nodejs中的异步、非阻塞i-o是如何实现的"><a href="#nodejs中的异步、非阻塞i-o是如何实现的" class="header-anchor">#</a> nodejs中的异步、非阻塞I/O是如何实现的</h1> <p>在听到 nodejs 相关的特性时，经常会对 <code>异步I/O</code>、<code>非阻塞I/O</code>有所耳闻，听起来好像是差不多的意思，但其实是两码事，下面我们就以原理的角度来剖析一下对 nodejs 来说，这两种技术底层是如何实现的？</p> <h2 id="什么是i-o？"><a href="#什么是i-o？" class="header-anchor">#</a> 什么是I/O？</h2> <p>首先，我想有必要把 I/O 的概念解释一下。I/O 即Input/Output, 输入和输出的意思。在浏览器端，只有一种 I/O，那就是利用 Ajax 发送网络请求，然后读取返回的内容，这属于网络I/O。回到 nodejs 中，其实这种的 I/O 的场景就更加广泛了，主要分为两种:</p> <ul><li>文件 I/O。比如用 fs 模块对文件进行读写操作。</li> <li>网络 I/O。比如 http 模块发起网络请求。</li></ul> <h2 id="阻塞和非阻塞i-o"><a href="#阻塞和非阻塞i-o" class="header-anchor">#</a> 阻塞和非阻塞I/O</h2> <p><code>阻塞</code>和<code>非阻塞</code> I/O 其实是针对操作系统内核而言的，而不是 nodejs 本身。阻塞 I/O 的特点就是一定要<strong>等到操作系统完成所有操作后才表示调用结束</strong>，而非阻塞 I/O 是调用后立马返回，不用等操作系统内核完成操作。</p> <p>对前者而言，在操作系统进行 I/O 的操作的过程中，我们的应用程序其实是一直处于等待状态的，什么都做不了。那如果换成非阻塞I/O，调用返回后我们的 nodejs 应用程序可以完成其他的事情，而操作系统同时也在进行 I/O。这样就把等待的时间充分利用了起来，提高了执行效率，但是同时又会产生一个问题，nodejs 应用程序怎么知道操作系统已经完成了 I/O 操作呢？</p> <p>为了让 nodejs 知道操作系统已经做完 I/O 操作，需要重复地去操作系统那里判断一下是否完成，这种重复判断的方式就是<strong>轮询</strong>。对于轮询而言，有以下这么几种方案:</p> <ol><li><p>一直轮询检查I/O状态，直到 I/O 完成。这是最原始的方式，也是性能最低的，会让 CPU 一直耗用在等待上面。其实跟阻塞 I/O 的效果是一样的。</p></li> <li><p>遍历文件描述符(即 文件I/O 时操作系统和 nodejs 之间的文件凭证)的方式来确定 I/O 是否完成，I/O完成则文件描述符的状态改变。但 CPU 轮询消耗还是很大。</p></li> <li><p>epoll模式。即在进入轮询的时候如果I/O未完成CPU就休眠，完成之后唤醒CPU。</p></li></ol> <blockquote><p>nodejs 应用程序发起 I/O 调用后可以直接去执行别的逻辑，操作系统默默地做完 I/O 之后给 nodejs 发一个完成信号，nodejs 执行回调操作。</p></blockquote> <p>这是理想的情况，也是异步 I/O 的效果，那如何实现这样的效果呢？</p> <h2 id="异步-i-o-的本质"><a href="#异步-i-o-的本质" class="header-anchor">#</a> 异步 I/O 的本质</h2> <p>Linux 原生存在这样的一种方式，即(AIO), 但两个致命的缺陷:</p> <ol><li>只有 Linux 下存在，在其他系统中没有异步 I/O 支持。</li> <li>无法利用系统缓存。</li></ol> <h3 id="nodejs中的异步-i-o-方案"><a href="#nodejs中的异步-i-o-方案" class="header-anchor">#</a> nodejs中的异步 I/O 方案</h3> <p>是不是没有办法了呢？在单线程的情况下确实是这样，但是如果把思路放开一点，利用多线程来考虑这个问题，就变得轻松多了。我们可以让一个进程进行计算操作，另外一些进行 I/O 调用，I/O 完成后把信号传给计算的线程，进而执行回调，这不就好了吗？没错，<strong>异步 I/O 就是使用这样的线程池来实现的</strong>。</p> <p>只不过在不同的系统下面表现会有所差异，在 Linux 下可以直接使用线程池来完成，在Window系统下则采用 IOCP 这个系统API(其内部还是用线程池完成的)。</p> <p>有了操作系统的支持，那 nodejs 如何来对接这些操作系统从而实现异步 I/O 呢？</p> <p>以文件为 I/O 我们以一段代码为例:</p> <div class="language-js extra-class"><pre class="language-js"><code><span class="token keyword">let</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

fs<span class="token punctuation">.</span><span class="token function">readFile</span><span class="token punctuation">(</span><span class="token string">'/test.txt'</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">err<span class="token punctuation">,</span> data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span> 
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div><h3 id="执行流程"><a href="#执行流程" class="header-anchor">#</a> 执行流程</h3> <p>执行代码的过程中大概发生了这些事情:</p> <ol><li>首先，fs.readFile调用Node的核心模块fs.js ；</li> <li>接下来，Node的核心模块调用内建模块node_file.cc，创建对应的文件I/O观察者对象(这个对象后面有大用！) ；</li> <li>最后，根据不同平台（Linux或者window），内建模块通过libuv中间层进行系统调用</li></ol> <p><a data-fancybox="" title="result" href="/images/js-async-001.jpg"><img src="/images/js-async-001.jpg" alt="result"></a></p> <h3 id="libuv调用过程拆解"><a href="#libuv调用过程拆解" class="header-anchor">#</a> libuv调用过程拆解</h3> <p>重点来了！libuv 中是如何来进行进行系统调用的呢？也就是 uv_fs_open() 中做了些什么？</p> <h4 id="_1-创建请求对象"><a href="#_1-创建请求对象" class="header-anchor">#</a> 1. 创建请求对象</h4> <p>以Windows系统为例来说，在这个函数的调用过程中，我们创建了一个文件I/O的请求对象，并往里面注入了回调函数。</p> <div class="language- extra-class"><pre class="language-text"><code>req_wrap-&gt;object_-&gt;Set(oncomplete_sym, callback);
</code></pre></div><p>req_wrap 便是这个请求对象，req_wrap 中 object_ 的 oncomplete_sym 属性对应的值便是我们 nodejs 应用程序代码中传入的回调函数。</p> <h4 id="_2-推入线程池，调用返回"><a href="#_2-推入线程池，调用返回" class="header-anchor">#</a> 2. 推入线程池，调用返回</h4> <p>在这个对象包装完成后，QueueUserWorkItem() 方法将这个对象推进线程池中等待执行。</p> <p>好，至此现在js的调用就直接返回了，我们的 js 应用程序代码可以继续往下执行，当然，当前的 I/O 操作同时也在线程池中将被执行，这不就完成了异步么：）</p> <p>等等，别高兴太早，回调都还没执行呢！接下来便是执行回调通知的环节。</p> <h4 id="_3-回调通知"><a href="#_3-回调通知" class="header-anchor">#</a> 3. 回调通知</h4> <p>事实上现在线程池中的 I/O 无论是阻塞还是非阻塞都已经无所谓了，因为异步的目的已经达成。重要的是 I/O 完成后会发生什么。</p> <p>在介绍后续的故事之前，给大家介绍两个重要的方法: GetQueuedCompletionStatus 和 PostQueuedCompletionStatus。</p> <ol><li><p>还记得之前讲过的 eventLoop 吗？在每一个Tick当中会调用GetQueuedCompletionStatus检查线程池中是否有执行完的请求，如果有则表示时机已经成熟，可以执行回调了。</p></li> <li><p>PostQueuedCompletionStatus方法则是向 IOCP 提交状态，告诉它当前I/O完成了。</p></li></ol> <p>名字比较长，先介绍是为了让大家混个脸熟，至少后面出来不会感到太突兀：）</p> <p>我们言归正传，把后面的过程串联起来。</p> <p>当对应线程中的 I/O 完成后，会将获得的结果存储起来，保存到相应的请求对象中，然后调用PostQueuedCompletionStatus()向 IOCP 提交执行完成的状态，并且将线程还给操作系统。一旦 EventLoop 的轮询操作中，调用GetQueuedCompletionStatus检测到了完成的状态，就会把请求对象塞给I/O观察者(之前埋下伏笔，如今终于闪亮登场)。</p> <p>I/O 观察者现在的行为就是取出请求对象的存储结果，同时也取出它的oncomplete_sym属性，即回调函数(不懂这个属性的回看第1步的操作)。将前者作为函数参数传入后者，并执行后者。 这里，回调函数就成功执行啦！</p> <div class="custom-block tip"><p class="custom-block-title">总结 :</p> <ol><li>阻塞和非阻塞 I/O 其实是针对操作系统内核而言的。阻塞 I/O 的特点就是一定要等到操作系统完成所有操作后才表示调用结束，而非阻塞 I/O 是调用后立马返回，不用等操作系统内核完成操作。</li> <li>nodejs中的异步 I/O 采用多线程的方式，由 <code>EventLoop</code>、<code>I/O 观察者</code>，<code>请求对象</code>、<code>线程池</code>四大要素相互配合，共同实现。</li></ol></div></div> <footer class="page-edit"><!----> <!----></footer> <div class="page-nav"><p class="inner"><span class="prev">
      ←
      <a href="/js-v8/002.html" class="prev">
        02.V8 引擎如何进行垃圾内存的回收？
      </a></span> <span class="next"><a href="/js-async/002.html">
        02.JS异步编程有哪些方案？为什么会出现这些方案？
      </a>
      →
    </span></p></div> </main></div><div class="global-ui"></div></div>
    <script src="/assets/js/app.e358a08d.js" defer></script><script src="/assets/js/2.88fa18d1.js" defer></script><script src="/assets/js/36.88b681f1.js" defer></script>
  </body>
</html>
